PART 07의 마무리는 지금까지의 조각을 모은 1:N 대용량 얼굴 검색 시스템입니다. InsightFace로 임베딩을 뽑고(2장), faiss로 색인해(4장), 질의 얼굴과 가장 닮은 사람을 찾습니다. PART 06의 find가 수천 명용이었다면, 이 시스템은 수십만 명까지 확장됩니다.
시스템의 두 단계
얼굴 검색 시스템은 등록(인덱스 구축)과 검색의 두 단계로 나뉩니다. 등록은 한 번 또는 가끔, 검색은 자주 일어납니다.
1단계 — 갤러리 인덱스 구축
인물별 폴더의 사진에서 임베딩을 모아 faiss 인덱스를 만들고 저장합니다.
# 파일: build_index.py"""갤러리에서 InsightFace 임베딩을 모아 faiss 인덱스를 만든다."""import osimport globimport pickleimport numpy as npimport cv2import faissfrom insightface.app import FaceAnalysisapp = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"])app.prepare(ctx_id=-1, det_size=(640, 640))embeddings, names = [], []for person in sorted(os.listdir("gallery")): for path in glob.glob(os.path.join("gallery", person, "*.jpg")): faces = app.get(cv2.imread(path)) if not faces: continue embeddings.append(faces[0].normed_embedding) # 정규화 임베딩 names.append(person)X = np.array(embeddings, dtype="float32")faiss.normalize_L2(X) # 안전하게 한 번 더 정규화index = faiss.IndexFlatIP(512)index.add(X)faiss.write_index(index, "faces.index") # 인덱스 저장with open("names.pkl", "wb") as f: # 같은 순서의 이름 저장 pickle.dump(names, f)print(f"등록 완료: {len(names)}건, 인물 {len(set(names))}명")
normed_embedding을 모아 IndexFlatIP에 넣고, 인덱스와 이름을 파일로 저장합니다. 이름은 임베딩과 같은 순서로 저장하는 것이 핵심입니다. 검색 결과가 위치 번호로 나오므로, 그 번호로 이름을 찾기 때문입니다.
2단계 — 질의 검색
저장한 인덱스를 불러와 질의 얼굴을 검색합니다.
# 파일: search_query.py"""저장한 인덱스로 질의 얼굴의 신원을 찾는다."""import pickleimport numpy as npimport cv2import faissfrom insightface.app import FaceAnalysisapp = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"])app.prepare(ctx_id=-1, det_size=(640, 640))index = faiss.read_index("faces.index")with open("names.pkl", "rb") as f: names = pickle.load(f)faces = app.get(cv2.imread("query.jpg"))for face in faces: q = np.array([face.normed_embedding], dtype="float32") faiss.normalize_L2(q) D, I = index.search(q, k=3) # top-3 후보 best_sim, best_idx = float(D[0][0]), int(I[0][0]) if best_sim > 0.4: # 코사인 임계값(데이터로 조정) print(f"식별: {names[best_idx]} (유사도 {best_sim:.3f})") else: print(f"미등록자 (최고 유사도 {best_sim:.3f})")
질의 임베딩을 정규화해 search하면 가장 닮은 후보들이 나옵니다. 첫 후보의 유사도가 임계값(0.4)을 넘으면 그 이름으로 식별하고, 못 넘으면 미등록자로 처리합니다. PART 04의 1:N 식별과 임계값 판정이 그대로, 그러나 대규모에서도 빠르게 동작합니다.
실전을 위한 보강
이 시스템을 실제 운영하려면 몇 가지를 더합니다.
- 여러 장 등록: 인물당 여러 사진의 임베딩을 모두 넣으면 다양한 각도에 강해집니다(PART 05 DB와 같은 원리).
- 품질 필터:
det_score가 낮은 흐릿한 얼굴은 등록·검색에서 제외해 노이즈를 줄입니다. - 위조 방지: 사진·화면을 들이대는 부정 인증을 막으려면 라이브니스 검사가 필요합니다(PART 09).
- 개인정보: 임베딩과 이름은 민감 정보이므로 접근 통제·보관 기간을 명확히 합니다(PART 11).
실무 팁. 대규모 검색 시스템에서 가장 흔한 실수는 "임베딩과 이름의 순서가 어긋나는 것"입니다. 등록 중 얼굴을 못 찾아 건너뛴 사진이 있으면, 임베딩은 빠졌는데 이름은 그대로여서 번호가 밀립니다. 위 코드처럼 임베딩을 추가할 때만 이름도 함께 추가하면(둘을 같은 루프에서 append) 이 어긋남을 원천 차단할 수 있습니다.
이 장에서 기억할 것
1:N 대용량 검색 시스템은 등록(InsightFace 임베딩 → faiss 인덱스 저장)과 검색(질의 임베딩 → top-k → 임계값 판정)의 두 단계로 만듭니다. normed_embedding과 IndexFlatIP로 코사인 검색을 하고, 임베딩과 이름을 같은 순서로 관리하는 것이 핵심입니다. 실전화에는 다중 등록·품질 필터·위조 방지·개인정보 보호가 더해집니다. 이로써 PART 07이 끝납니다. 다음 PART 08에서는 신원을 넘어 표정과 얼굴 속성, 곧 감정 인식으로 들어갑니다.